package abacab;

import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;

import abacab.actor.ActorDef;
import abacab.ext.DependsOn;
import abacab.ext.Extension;
import abacab.ext.ExtensionProvider;
import abacab.ref.ActorLocator;
import abacab.ref.ActorLocatorImpl;
import abacab.ref.ActorRef;

import com.typesafe.config.Config;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigException.BadValue;
import com.typesafe.config.ConfigFactory;

/**
 * Represents an actor system
 */
public abstract class ActorSystem {
	/** The charset */
	public static final Charset															CHARSET						= Charset.forName("UTF-8");

	/** The extensions' configuration path */
	private static final String															ABACAB_EXTENSIONS	= "abacab.extensions";

	/** The existing systems */
	private static final ConcurrentMap<String, ActorSystem>	systems						= new ConcurrentHashMap<String, ActorSystem>();

	/** The locators */
	private static final List<ActorLocator>									locators					= new CopyOnWriteArrayList<ActorLocator>();
	static {
		locators.add(new ActorLocatorImpl());
	}

	/**
	 * Get an actor system
	 * 
	 * @param name
	 *          the system's name
	 * @return the actor system
	 */
	public static ActorSystem getActorSystem(String name) {
		return getActorSystem(name, ConfigFactory.load());
	}

	/**
	 * Get an actor system if it exists
	 * 
	 * @param name
	 *          the system's name
	 * @return the actor system or NULL if it does not exist
	 */
	public static ActorSystem getActorSystemIfExists(String name) {
		return systems.get(name);
	}

	/**
	 * Get an actor system
	 * 
	 * @param name
	 *          the system's name
	 * @param config
	 *          a specific configuration (used only when creating the system - ignored otherwise)
	 * @return the actor system
	 */
	public static ActorSystem getActorSystem(String name, Config config) {
		ActorSystem system = systems.get(name);
		if (system == null) {
			system = new ActorSystemImpl(name, config);
			ActorSystem concManager = systems.putIfAbsent(name, system);
			if (concManager == null) {
				// Initialize the actor system
				system.init();
			}
			else {
				system = concManager;
			}
		}
		return system;
	}

	/**
	 * Find an actor for a particular path
	 * 
	 * @param path
	 *          the path
	 * @return an actor reference if found or NULL
	 * @throws IllegalArgumentException
	 *           if the path is invalid
	 */
	public static <M> ActorRef<M> findActor(String path) throws IllegalArgumentException {
		for (ActorLocator locator : locators) {
			ActorRef<M> ref = locator.locateActor(path);
			if (ref != null) {
				return ref;
			}
		}
		return null;
	}

	/**
	 * Register a locator for non local actors
	 * 
	 * @param locator
	 *          the locator
	 */
	public static void registerLocator(ActorLocator locator) {
		locators.add(locator);
	}

	// **************************************************************************
	// **************************************************************************
	// SYSTEM BASE IMPLEMENTATION
	// **************************************************************************
	// **************************************************************************

	/** The system's name */
	private final String									name;

	/** The configuration */
	private final Config									config;

	/** The available extensions */
	private final Map<String, Extension>	extensions;

	/**
	 * Create the system
	 * 
	 * @param name
	 *          the system's name
	 * @param config
	 *          the configuration
	 */
	protected ActorSystem(String name, Config config) {
		this.name = name;
		this.config = config;
		this.extensions = new IdentityHashMap<String, Extension>();
	}

	/**
	 * Initialize the system
	 * 
	 * @throws ConfigException
	 *           if the extensions are not found (should not happen since the default "reference.conf" contains it) or one of the extensions cannot be built
	 */
	@SuppressWarnings("unchecked")
	protected final void init() throws ConfigException {
		List<String> extensionsList = this.config.getStringList(ABACAB_EXTENSIONS);
		for (String extProvider : extensionsList) {
			try {
				Class<? extends ExtensionProvider> providerClass = (Class<? extends ExtensionProvider>) Class.forName(extProvider);
				loadExtension(providerClass, null);
			}
			catch (ClassNotFoundException e) {
				throw new BadValue(ABACAB_EXTENSIONS, "Extension not found: " + extProvider);
			}
			catch (ClassCastException e) {
				throw new BadValue(ABACAB_EXTENSIONS, "Extension \"" + extProvider + "\" does not implement \"" + ExtensionProvider.class.getName() + "\"");
			}
			catch (Exception e) {
				throw new BadValue(ABACAB_EXTENSIONS, "Unable to load the extension \"" + extProvider + "\"", e);
			}
		}
	}

	/**
	 * Load an extension
	 * 
	 * @param providerClass
	 *          the extension's provider
	 * @param stack
	 *          a stack to prevent cyclic dependencies
	 * @throws Exception
	 *           if the extension was unloadable
	 */
	private final void loadExtension(Class<? extends ExtensionProvider> providerClass, Set<Class<? extends ExtensionProvider>> stack) throws Exception {
		DependsOn dependsOn = providerClass.getAnnotation(DependsOn.class);
		if (dependsOn != null && dependsOn.value().length > 0) {
			if (stack == null) {
				stack = new HashSet<Class<? extends ExtensionProvider>>();
			}
			stack.add(providerClass);
			for (Class<? extends ExtensionProvider> dependency : dependsOn.value()) {
				if (stack.contains(dependency)) {
					StringBuilder sb = new StringBuilder(100);
					sb.append("Cyclic dependency detected: ");
					for (Class<? extends ExtensionProvider> s : stack) {
						sb.append(s.getName()).append(", ");
					}
					throw new IllegalStateException(sb.toString());
				}
				loadExtension(dependency, stack);
			}
			stack.remove(providerClass);
		}
		ExtensionProvider provider = providerClass.newInstance();
		Extension extension = provider.create(this);
		this.extensions.put(provider.getId(), extension);
	}

	/**
	 * @return the path to the system
	 */
	public String getPath() {
		return "/" + this.name;
	}

	/**
	 * @return the configuration
	 */
	public Config getConfig() {
		return this.config;
	}

	/**
	 * Get an extension
	 * 
	 * @param provider
	 *          the extension provider
	 * @return the extension or NULL if none found
	 */
	@SuppressWarnings("unchecked")
	public <E extends Extension, P extends ExtensionProvider<E>> E getExtension(P provider) {
		return (E) this.extensions.get(provider.getId());
	}

	/**
	 * Shutdown all actors and executors
	 */
	public abstract void shutdown();

	/**
	 * Create an actor
	 * 
	 * @param reference
	 *          the actor's reference name
	 * @param factory
	 *          the factory to create the actor and its context
	 * @return a reference to the actor
	 * @throws ConfigException
	 *           if the configuration is invalid
	 */
	public abstract <M> ActorRef<M> createActor(String reference, ActorDef<M> factory) throws ConfigException;

	/**
	 * Get an existing actor
	 * 
	 * @param reference
	 *          the actor's reference name
	 * @return a reference to the actor
	 */
	public abstract <M> ActorRef<M> getActor(String reference);
}